/*
 * Copyright (c) 2012 Sean Connelly
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 * associated documentation files (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge, publish, distribute,
 * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or
 * substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
 * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

var path = require('path');
var fs = require('fs');
var common = require('./common.js');
var tree = require('./tree.js');
var config = require('./config.js');
var preMetatypeTree = require('./pre-metatype-tree.js').value;
var textStream = require('./text-stream.js');
var lexRho = require('./lex-rho.js');

function parseRho_createParseObj(tokens_orig)
{
	var tokens = [];
	// MYTODO: filter out comments... might be a better way to do this...
	(function() {
			for (var i = 0; i < tokens_orig.length; i++)
			{
				if (!tokens_orig[i].isCommentLine() &&
					!tokens_orig[i].isCommentBlock() &&
					!tokens_orig[i].isCommentNest())
				{
					tokens.push(tokens_orig[i]);
				}
			}
		})();
	var token_i = 0;
	return {
			// get the next token, or false if EOF
			next: function() {
					if (token_i >= tokens.length)
						return false;
					//console.log(tokens[token_i].tokenName, tokens[token_i].chars);
					token_i++;
					return tokens[token_i - 1];
				},

			// next, but error out if EOF
			nextToken: function() {
					var n = this.next();
					if (n === false)
						throw 'MYTODO: invalid end-of-file';
					return n;
				},

			// peek at the next token without moving to it, or false if EOF
			peek: function() {
					if (token_i >= tokens.length)
						return false;
					return tokens[token_i];
				},

			// peek, but error out if EOF
			peekToken: function() {
					var p = this.peek();
					if (p === false)
						throw 'MYTODO: invalid end-of-file';
					return p;
				},

			isNextSpecial: function(ch) {
					var p = this.peek();
					if (p === false)
						return false;
					return p.isSpecial() && p.chars == ch;
				},

			isNextAssignOp: function() {
					var p = this.peek();
					if (p === false)
						return false;
					return p.isSpecial() && config.isAssignOp(p.chars);
				},

			isNextKeyword: function(ch) {
					var p = this.peek();
					if (p === false)
						return false;
					return p.isKeyword() && (typeof ch === 'undefined' || p.chars == ch);
				},

			isNextIdent: function() {
					var p = this.peek();
					if (p === false)
						return false;
					return p.isIdent();
				},

			isNextRident: function() {
					var p = this.peek();
					if (p === false)
						return false;
					return p.isIdent() || p.isRhodent();
				},

			isNextRhodent: function(ch) {
					var p = this.peek();
					if (p === false)
						return false;
					return p.isRhodent() && p.chars == ch;
				},

			expectKeyword: function(kw) {
					var tok = this.next();
					if (tok === false)
						throw 'MYTODO: missing keyword ' + kw;
					if (!tok.isKeyword())
						throw 'MYTODO: missing keyword ' + kw;
					if (tok.chars !== kw)
						throw 'MYTODO: missing keyword ' + kw;
					return tok;
				},

			expectIdent: function() {
					var tok = this.next();
					if (tok === false)
						throw 'MYTODO: missing ident';
					if (!tok.isIdent())
						throw 'MYTODO: missing ident';
					return tok;
				},

			expectRident: function() {
					var tok = this.next();
					if (tok === false)
						throw 'MYTODO: missing rident';
					if (!tok.isIdent() && !tok.isRhodent())
						throw 'MYTODO: missing rident';
					return tok;
				},

			expectSpecial: function(ch) {
					var tok = this.next();
					if (tok === false)
						throw 'MYTODO: missing special ' + ch;
					if (!tok.isSpecial())
						throw 'MYTODO: missing special ' + ch;
					if (tok.chars !== ch)
						throw 'MYTODO: missing special ' + ch;
					return tok;
				},

			setVersion: function(ver_tok) {
					// MYTODO: do... something?
				},

			StatementList: function(hasEOF) {
					var t = tree.createBox('StatementList');
					while (true)
					{
						var s = this.Statement();
						if (s === false) // if no valid statement
						{
							// MYTODO: stuff
							if (hasEOF && this.peek() !== false)
								throw 'MYTODO: no more valid statements, but still have tokens: ' + this.next();
							return t;
						}
						else if (common.isArray(s))
						{
							for (var i = 0; i < s.length; i++)
								t.addTree(s[i]);
						}
						else
							t.addTree(s);
					}
				},

			ClassStatementList: function() {
					var t = tree.createBox('ClassStatementList');
					while (true)
					{
						var s = this.ClassStatement();
						if (s === false) // if no valid statement
						{
							// MYTODO: stuff
							return t;
						}
						else if (common.isArray(s))
						{
							for (var i = 0; i < s.length; i++)
								t.addTree(s[i]);
						}
						else
							t.addTree(s);
					}
				},

			Statement: function() {
					var p = this.peek();
					if (p === false)
						return false;

					if (p.isIdent() || p.isRhodent())
						return this.LeadingRident(this.RetrieveRident());
					else if (p.isKeyword() || p.isSpecial())
					{
						switch (p.chars)
						{
						case 'bounce': return this.BounceBlock();
						case 'define': return this.DefineBlock();
						case 'depart': return this.Depart();
						case 'do':     return this.DoBlock();
						case 'eval':   return this.Eval();
						case 'exit':   return this.Exit();
						case 'goto':   return this.Goto();
						case 'if':     return this.IfBlock();
						case 'import': return this.Import();
						case 'next':   return this.Next();
						case 'return': return this.Return();
						case 'switch': return this.SwitchBlock();
						case 'throw':  return this.Throw();
						case 'try':    return this.TryBlock();
						case 'when':   return this.WhenBlock();
						case '>>':     return this.Label();
						case '&':      return this.PlainEval();
						case 'rhox':   return this.RhoxStatement(); // only found when parsing rhox files
						}
					}

					return false;
				},

			RhoxStatement: function() {
					// There is a need to execute special instructions in rho files, *only* to be used
					// during the compile process, and NOT part of the official rho specification...
					// for this reason, "rhox" was created (RHO eXtended).
					//
					// When the `extended` flag is set in the lexer, it will begin identifying "rhox" as
					// a keyword (otherwise, it will identify it as an Ident, according to the Rho spec).
					//
					// That will trigger Statement (above) to call this function when seeing "rhox" as a
					// keyword.
					//
					// This function can basically do whatever is needed in order to bootstrap the
					// compiler, WITHOUT affecting the rho specification.

					this.expectKeyword('rhox');
					var cmd = this.nextToken();
					if (typeof cmd.chars == 'undefined')
						throw 'MYTODO: internal error -- rhox statement must have a command after it (with chars!)';

					var ret = false;
					switch (cmd.chars)
					{
					case 'include':
						var filename = this.basicText().chars;
						var ast = parseRho_parse(
									lexRho.lexExtended( // lexExtended because it's rhox (*not* rho)
											textStream.create(
													fs.readFileSync(
															path.resolve(__dirname, '../' + filename),
															'utf8'
														)
												)
										)
								);
						// MYTODO: confirm ast version and such (?)
						ret = [];
						ast.toChild('Program').each(function() {
								ret.push(this);
							});
						break;
					}

					return ret === false ? this.Statement() : ret;
				},

			ClassStatement: function() { // read a list of define/import/declaration statments, return a tree or false
					// MYTODO: probably rewrite this whole function to confirm it is up-to-date
					if (this.isNextKeyword('define'))
						return this.DefineBlock();
					else if (this.isNextKeyword('import'))
						return this.Import();
					else if (this.isNextKeyword('bounce'))
						return this.BounceBlock();
					else if (this.isNextRident() && !this.isNextRhodent('my'))
						return this.NewVariable(this.Type()).updateData({ myTok: false, overrideTok: false });
					// check for `[override] my int x` or `[override] my define blah static(blah) blah end`
					var overrideTok = false;
					var myTok = false;
					if (this.isNextKeyword('override'))
					{
						overrideTok = this.next();
						if (!this.isNextRhodent('my'))
							throw 'MYTODO: expecting `my`';
						myTok = this.next();
					}
					else if (this.isNextRhodent('my'))
						myTok = this.next();
					if (myTok !== false)
					{
						if (this.isNextKeyword('define'))
							return this.DefineBlock().updateData({ myTok: myTok, overrideTok: overrideTok });
						else if (this.isNextRident())
							return this.NewVariable(this.Type()).updateData({ myTok: myTok, overrideTok: overrideTok });
						throw 'MYTYDO: expecting declaration in class';
					}
					return false;
				},

			BounceBlock: function() { // read a bounce-block, return a tree
					var bt = this.expectKeyword('bounce');
					var t = this.StatementList(false);
					var et = this.expectKeyword('end');
					return tree.createNode('BounceBlock', { startTok: bt, endTok: et })
						.setTree('StatementList', t);
				},

			DefineTemplate: function() {
					var otok = this.expectSpecial('#[');
					var tmps = tree.createBox('TemplateNameList', { openTok: otok });
					while (!this.isNextSpecial(']'))
					{
						tmps.addNode('TemplateName', { name: this.expectRident() });
						if (!this.isNextSpecial(','))
							break;
						this.next(); // consume the comma
					}
					tmps.updateData({ closeTok: this.expectSpecial(']') });
					return tmps;
				},

			buildDefineParamList: function(mtInfo) {
					// build the parameter list based on mtInfo.params
					var buildParam = [];
					var lastParam = false;
					for (var i = 0; i < mtInfo.params.length; i++)
					{
						var p = mtInfo.params[i];
						if (p.substr(0, 1) == '*')
						{
							if (i != mtInfo.params.length - 1)
							{
								throw 'MYTODO: internal error: metatype ' + mt.chars +
									' has a starred parameter ' + p +
									' in the middle of the parameter list';
							}
							lastParam = p.substr(1);
						}
						else
							buildParam.push(p);
					}

					return {
							buildParam: buildParam,
							lastParam: lastParam
						};
				},

			getDefinedParameter: function(t, p) {
					if (p == 'type')
						t.addTree(this.Type());
					else if (p == 'expr')
						t.addTree(this.ExpressionNoComma());
					//else if (p == 'macro') // MYTODO: this
						// something
					else
						throw 'MYTODO: internal error: unknown metatype parameter type: ' + p + ' for metatype: ' + mt.chars;
					return true;
				},

			MetatypeParamList: function(buildParam, lastParam) {
					this.expectSpecial('(');
					var t = tree.createBox('MetatypeParamList');
					for (var i = 0; i < buildParam.length; i++)
					{
						if (i > 0)
							this.expectSpecial(',');
						do {
							if (this.getDefinedParameter(t, buildParam[i]) === true) // loop until getParam finds what it's looking for
								break;
							i++;
						} while (i < buildParam.length);
					}
					if (lastParam === false)
					{
						if (this.isNextSpecial(',')) // consume optional comma
							this.next();
					}
					else
					{
						while (true)
						{
							if (this.isNextSpecial(')'))
								break;
							this.getDefinedParameter(t, lastParam);
							if (this.isNextSpecial(','))
								this.next();
							else
								break;
						}
					}
					this.expectSpecial(')');
					return t;
				},

			DefineBlock: function() { // read a define statement, return a tree
					var deftok = this.expectKeyword('define');
					var defname = this.expectRident();
					var t = tree.createNode('DefineBlock', { defineTok: deftok, name: defname });

					if (this.isNextSpecial('#['))
						t.setTree('TemplateNameList', this.DefineTemplate());

					var mt = this.expectRident();
					if (!preMetatypeTree.exists(mt.chars))
						throw 'MYTODO: invalid metatype: ' + mt.chars;
					var mtInfo = preMetatypeTree.to(mt.chars).getData();
					t.updateData({ metatype: mt });

					var dpl = this.buildDefineParamList(mtInfo);

					// execute the parameter list of buildParam and lastParam
					t.setTree('MetatypeParamList', this.MetatypeParamList(dpl.buildParam, dpl.lastParam));

					switch (mtInfo.bodyType)
					{
					// MYTODO: each of these
					//case 'macro':
					//case 'struct':
					//case 'classface':
					case 'class':
						t.setTree('DefineBody', this.ClassStatementList());
						break;
					case 'symbols':
						var syms = [];
						while (this.isNextIdent())
							syms.push(this.next());
						t.updateData({ symbols: syms });
						break;
					case 'static':
						t.setTree('DefineBody', this.Expression()); // MYTODO: ExpressionNoComma? TrailComma?
						break;
					case 'none':
						break;
					default:
						throw 'MYTODO: internal error, unknown body type: ' + mtInfo.bodyType;
					}
					this.expectKeyword('end');
					return t;
				},

			DoBlock: function() { // read a do-block, return a tree
					var dot = this.expectKeyword('do');
					var st1 = this.StatementList(false);
					if (this.isNextKeyword('while'))
					{
						var wht = this.expectKeyword('while');
						var cond = this.Condition();
						var et = this.expectKeyword('end');
						return tree.createNode('DoWhileBlock', { doTok: dot, whileTok: wht, endTok: et })
							.setTree('StatementList', st1)
							.setTree('Condition', cond);
					}
					else if (this.isNextKeyword('end'))
					{
						var et = this.expectKeyword('end');
						return tree.createNode('DoBlock', { doTok: dot, endTok: et })
							.setTree('StatementList', st1);
					}
					// else, unexpected token
					throw 'MYTODO: expected either `end` or `while`';
				},

			Eval: function() {
					var n = this.next();
					if (n === false || !n.isKeyword() || n.chars !== 'eval')
						throw 'MYTODO: expecting `eval` keyword';
					return tree.createNode('Eval').setTree('PlainEval', this.PlainEval());
				},

			IfBlock: function() { // read an if-block, return a tree
					var t = tree.createNode('IfBlock', { ifTok: this.expectKeyword('if') })
						.toSetBox('ConditionList')
						.addTree(this.Condition(false));
					var elseifs = [];
					while (this.isNextKeyword('elseif'))
					{
						elseifs.push(this.next()); // consume 'elseif'
						t.addTree(this.Condition());
					}
					t = t.toParent();
					if (elseifs.length > 0)
						t.updateData({ elseifToks: elseifs });
					if (this.isNextKeyword('else'))
					{
						t.updateData({ elseTok: this.expectKeyword('else') })
							.setTree('Else', this.StatementList(false));
					}
					return t.updateData({ endTok: this.expectKeyword('end') });
				},

			basicText: function() { // returns a Text token
					var tok = this.nextToken();
					if (!tok.isBeginText())
						throw 'MYTODO: expecting a string here';
					var level = 1;
					var toks = [];
					while (level > 0)
					{
						toks.push(this.nextToken());
						if (toks[toks.length - 1].isBeginText())
							level++;
						else if (toks[toks.length - 1].isEndText())
							level--;
					}
					toks.pop(); // remove the final EndText token

					if (toks.length != 1 || !toks[0].isText())
						throw 'MYTODO: expecting a plain string here, with no interpolation';

					return toks[0];
				},

			Import: function() {
					var t = tree.createBox('Import', { importTok: this.expectKeyword('import') });
					while (true)
					{
						if (this.isNextSpecial('<')) // rho library
						{
							var temp = t.toAddNode('ImportRho', { openTok: this.expectSpecial('<') })
								.updateData({ library: this.expectRident() })
								.updateData({ closeTok: this.expectSpecial('>') });
							if (this.isNextKeyword('as'))
							{
								temp.updateData({ asTok: this.expectKeyword('as') })
									.updateData({ namespace: this.expectIdent() });
							}
						}
						else if (this.peekToken().isBeginText())
						{
							var temp = t.toAddNode('ImportFile', { filename: this.basicText() });
							if (this.isNextKeyword('as'))
							{
								temp.updateData({ asTok: this.expectKeyword('as') })
									.updateData({ namespace: this.expectIdent() });
							}
						}
						if (!this.isNextSpecial(','))
							return t;
						this.next(); // consume ','
					}
				},

			Goto: function() { // read a goto, return a tree
					return tree.createNode('Goto', { gotoTok: this.expectKeyword('goto') })
						.setTree('GotoExpression', this.Expression());
				},

			Label: function() { // read a label, return a tree
					var bt = this.expectSpecial('>>');
					var ident = this.expectIdent();
					if (ident.chars == 'here') // disallow defining a "here" label
						throw 'MYTODO: cannot redefine `here` label';
					return tree.createNode('Label',
						{ arrowTok: bt, name: ident, colonTok: this.expectSpecial(':') });
				},

			Return: function() { // read a return body, return a tree
					return tree.createNode('Return', { returnTok: this.expectKeyword('return') })
						.setTree('ReturnExpression', this.Expression());
				},

			SwitchBlock: function() {
					var swt = this.expectKeyword('switch');
					var stype = false;

					if (this.isNextRident()) // if we have a type after the switch
						stype = this.Type();

					this.expectSpecial('(');
					var expr = this.ExpressionNoComma();
					this.expectSpecial(')');

					if (stype === false)
					{
						var cl = tree.createBox('CaseList');
						while (this.isNextKeyword('case'))
						{
							cl.toAddNode('Case', { caseTok: this.expectKeyword('case') })
								.setTree('CaseCondition', this.Condition())
						}

						var t = tree.createNode('SwitchBlockPlain', { switchTok: swt })
							.setTree('SwitchExpression', expr)
							.setTree('CaseList', cl);
						if (this.isNextKeyword('else'))
						{
							t.updateData({ elseTok: this.expectKeyword('else') })
								.setTree('Else', this.StatementList(false));
						}
						return t.updateData({ endTok: this.expectKeyword('end') });
					}
					// else, typed switch

					var csl = tree.createBox('CaseSymbolList');
					while (this.isNextKeyword('case'))
					{
						var csl_cs = csl.toAddNode('CaseSymbol', { caseTok: this.expectKeyword('case') });
						this.expectSpecial('(');
						csl_cs.updateData({ symbol: this.expectIdent() });
						this.expectSpecial(')');
						csl_cs.setTree('StatementList', this.StatementList(false));
					}
					
					return tree.createNode('SwitchBlockTyped', { switchTok: swt })
						.setTree('SwitchType', stype)
						.setTree('SwitchExpression', expr)
						.setTree('CaseSymbolList', csl)
						.updateData({ endTok: this.expectKeyword('end') });
				},

			WhenBlock: function() {
					var wht = this.expectKeyword('when');
					var wtype = false;

					if (this.isNextRident()) // if we have a type after the switch
						wtype = this.Type();

					this.expectSpecial('(');
					var expr = this.ExpressionNoComma();
					this.expectSpecial(')');

					var il = tree.createBox('IsList');
					while (this.isNextKeyword('is'))
					{
						var il_is = il.toAddNode('Is', { isTok: this.expectKeyword('is') });

						if (this.isNextKeyword('likely'))
							il_is.updateData({ likelyTok: this.expectKeyword('likely') });
						else if (this.isNextKeyword('unlikely'))
							il_is.updateData({ unlikelyTok: this.expectKeyword('unlikely') });

						this.expectSpecial('(');
						var totn = this.TypeOrTypeName(false);
						il_is.setTree('IsType', totn.type);
						if (totn.name !== false)
							il_is.updateData({ name: totn.name });
						this.expectSpecial(')');

						il_is.setTree('StatementList', this.StatementList(false));
					}

					if (wtype === false)
					{
						var t = tree.createNode('WhenBlockPlain', { whenTok: wht })
							.setTree('WhenExpression', expr)
							.setTree('IsList', il);
						if (this.isNextKeyword('else'))
						{
							t.updateData({ elseTok: this.expectKeyword('else') })
								.setTree('Else', this.StatementList(false));
						}
						return t.updateData({ endTok: this.expectKeyword('end') });
					}

					// else, unioned when
					return tree.createNode('WhenBlockTyped', { whenTok: wht })
						.setTree('WhenType', wtype)
						.setTree('WhenExpression', expr)
						.setTree('IsList', il)
						.updateData({ endTok: this.expectKeyword('end') });
				},

			PlainEval: function() { // read a basic expression-statement, return a tree
					return this.PartialPlainEval(this.Expression());
				},

			PartialPlainEval: function(t) {
					return tree.createNode('PlainEval').setTree('EvalExpression', this.PostExpression(t));
				},

			Condition: function() { // read a Condition, return a tree
					// [likely|unlikely] (condition) statements*
					var t = tree.createNode('Condition');

					if (this.isNextKeyword('likely'))
						t.updateData({ likelyTok: this.expectKeyword('likely') });
					else if (this.isNextKeyword('unlikely'))
						t.updateData({ unlikelyTok: this.expectKeyword('unlikely') });

					return t.updateData({ openTok: this.expectSpecial('(') })
						.setTree('ConditionExpression', this.ExpressionTrailComma())
						.updateData({ closeTok: this.expectSpecial(')') })
						.setTree('StatementList', this.StatementList(false));
				},

			RetrieveRident: function() { // get a rident, wrap it in a node, return the tree
					return tree.createNode('RetrieveRident', { rident: this.expectRident() });
				},

			LeadingRident: function(t) { // figure out if we are defining a new variable, or evaluating an expression
					// if this is a variable definition, then rident is part of a type
					// if this is something else, it's just some normal expression
					// so we need to read enough to determine which one it is... and if it's an expression, then we
					// need to jump into the MIDDLE of a ExpressionHead with the correct state... (it's a bit tricky)

					// keep looping around until we determine which it is (new variable, or expression-statement)
					while (true)
					{
						var p = this.peekToken();
						if (p.isSpecial())
						{
							if (p.chars == '#[') // could be a type
								t = this.IndexTemplate(t);
							else if (p.chars == '.') // could be a type
								t = this.IndexStatic(t);
							else
								break;
						}
						else if (p.isBeginMacro())
							break;
						else if (p.isRident()) // must be a new variable with `t` as the type
							return this.NewVariable(t);
						else
							throw 'MYTODO: invalid statement';
					}
					// must be an expression, where tree is the value on top of the stack
					// this.PostExpression is where we get the logic for assignment and "as"
					return this.PartialPlainEval(
						tree.createNode('Expression')
							.setTree('ExpressionHead',
								this.ExpressionHead(false, [], [t], true, false)));
				},

			NewVariable: function(firstTypeTree) { // figure out the new variable form, and initializers
					var firstName = this.expectIdent(); // all versions have a name here
					var p = this.peekToken();

					// check for sugar'ed code initializer
					if (p.isSpecial() && (p.chars === '&' || p.chars === '('))
					{
						return tree.createNode('NewCodeVariable', { name: firstName })
							.setTree('Type', firstTypeTree)
							.setTree('FunctionLiteral', this.FunctionLiteral());
					}

					var t = tree.createNode('NewVariableList')
						.toSetBox('TypeNameList')
						.toAddNode('TypeName', { name: firstName })
						.setTree('Type', firstTypeTree)
						.toParent();

					while (this.isNextSpecial(','))
					{
						this.next(); // consume the comma
						var type = this.Type();
						t.toAddNode('TypeName', { name: this.expectIdent() })
							.setTree('Type', type);
					}

					t = t.toParent();

					if (this.isNextSpecial(':'))
					{
						this.next(); // consume the colon
						t.setTree('InitExpression', this.Expression());
					}

					return t;
				},

			TryBlock: function() { // read a try-block, return a tree
					// try carry catch () end
					var tryt = this.expectKeyword('try');
					var st1 = this.StatementList(false);

					var cart = false;
					var st2 = false;
					if (this.isNextKeyword('carry'))
					{
						cart = this.expectKeyword('carry');
						st2 = this.StatementList(false);
					}

					var catt = this.expectKeyword('catch');
					var catp = tree.createBox('CatchParamList', { openTok: this.expectSpecial('(') });
					var foundVarg = false;
					do {
						var totn = this.TypeOrTypeName(!foundVarg); // totn ==> (T)ype (O)r (T)ype (N)ame

						catp = catp.toAddNode('CatchParam')
							.setTree('Type', totn.type);
						if (totn.name !== false)
							catp.updateData({ name: totn.name });
						catp = catp.toParent();

						foundVarg = totn.ddd;

						if (!this.isNextSpecial(','))
							break;
						this.next(); // consume ','
					} while (!this.isNextSpecial(')'));

					catp.updateData({ closeTok: this.expectSpecial(')') });

					var st3 = this.StatementList(false);
					var endt = this.expectKeyword('end');

					if (cart === false)
					{
						return tree.createNode('TryCatchBlock',
								{ tryTok: tryt, catchTok: catt, endTok: endt })
							.toSetNode('TryBlock')
								.setTree('TryStatementList', st1)
							.toParent()
							.toSetNode('CatchBlock')
								.setTree('CatchParamList', catp)
								.setTree('CatchStatementList', st3)
							.toParent();
					}
					// else, try-carry-catch block
					return tree.createNode('TryCarryCatchBlock',
							{ tryTok: tryt, carryTok: cart, catchTok: catt, endTok: endt })
						.toSetNode('TryCarryBlock')
							.setTree('TryStatementList', st1)
							.setTree('CarryStatementList', st2)
						.toParent()
						.toSetNode('CatchBlock')
							.setTree('CatchParamList', catp)
							.setTree('CatchStatementList', st3)
						.toParent();
				},

			ExpressionNoComma: function() { // read a singular expression (no commas), return a tree
					return tree.createNode('Expression').setTree('ExpressionHead',
						this.ExpressionHead(true, [], [], false, false));
				},

			Expression: function() { // read an expression (including commas), return a tree
					return tree.createNode('Expression').setTree('ExpressionHead',
						this.ExpressionHead(true, [], [], true, false));
				},

			ExpressionTrailComma: function() { // read an expression, possibly trailing a comma, return a tree
					return tree.createNode('Expression').setTree('ExpressionHead',
						this.ExpressionHead(true, [], [], true, true));
				},

			evalLeftFirst: function(leftUnary, op1, op2) {
					// returns true/false on whether the left operator should be evaluate first

					// op2 is always a binary operator
					if (!config.isBinaryOp(op2))
						throw 'MYTODO: internal error; unknown operator: ' + op2;

					if (leftUnary)
					{
						// op1 is a unary operator
						if (!config.isUnaryOp(op1))
							throw 'MYTODO: internal error; unknown operator: ' + op1;
						return !config.isUnaRightToLeft(op1 + ' ' + op2);
					}

					// else, op1 is a binary operator
					if (!config.isBinaryOp(op1))
						throw 'MYTODO: internal error; unknown operator: ' + op1;

					// special logic for operators of the same precedence
					if (config.binaryPrec[op1] == config.binaryPrec[op2])
					{
						if (config.isCompareOp(op1) && config.isCompareOp(op2))
						{
							// compare ops are right-to-left, in order to more easily detect a < b < c
							return false;
						}
						return !config.isBinRightToLeft(op1 + ' ' + op2);
					}

					// if they have different precedence, then favor the appropriate one
					return config.binaryPrec[op1] < config.binaryPrec[op2];
				},

			processBinaryOp: function(op, left, right) {
					// most binary operators are simple... but since we perform some tree
					// transformations for operators we temporarily treat as binary operators, we need
					// to filter those special operators out, and create the appropriate tree...
					//
					// specifically:
					// 1. Comparison operators
					// 2. Comma operator
					// 3. Then-Else operator
					//
					// `a < b < c < d` is collected as `a < (b < (c < d))`
					// this needs to be detected and grouped into a CompareOp
					//
					// `a, b, c, d` is collected as `a, (b, (c, d))`
					// this needs to be detected and grouped into a GroupOp
					//
					// `a then b else c` is collected as `a then (b else c)`
					// this needs to be detected and grouped into a ThenElse
					//
					// the logic for that is below...

					if (config.isCompareOp(op.getData().op.chars))
					{
						if (right.getName() == 'CompareOp' && // if our last operation was a compare op
							typeof right.getData().hasParens === 'undefined') // and it wasn't in parens
						{
							right.getData().opList.unshift(op.getData().op); // add the op to the op list
							return right.prependTree(left);
						}
						// otherwise, create the tree
						return tree.createBox('CompareOp', op.getData())
							.updateData({ opList: [op.getData().op] })
							.addTree(left)
							.addTree(right);
					}
					else if (op.getData().op.chars === ',')
					{
						if (right.getName() == 'GroupOp' && // if our last operation was a group op
							typeof right.getData().hasParens === 'undefined') // and it wasn't in parens
							return right.prependTree(left);
						// otherwise, create the tree
						return tree.createBox('GroupOp', op.getData())
							.addTree(left)
							.addTree(right);
					}
					else if (op.getData().op.chars == config.ternaryTestOp)
					{ // check for 'then' only, because `a then b else c` favors right in evalLeftFirst
						if (right.getData().op.chars !== config.ternaryElseOp || // make sure we have `a then b else c`
							typeof right.getData().hasParens !== 'undefined') // make sure we didn't have `a then (b else c)`
							throw 'MYTODO: invalid then-else';
						return tree.createNode('ThenElse')
							.setTree('ThenElseCondition', left)
							.setTree('ThenElseTrue', right.toChild('BinLeftValue'))
							.setTree('ThenElseFalse', right.toChild('BinRightValue'));
					}

					// otherwise, just apply the binary operator like normal
					return op.setTree('BinLeftValue', left).setTree('BinRightValue', right);
				},

			ExpressionHead: function(value_or_unary, op_stack, stack, canComma, canTrailComma) {
					var done = false;
					var thenCount = 0;
					var lastOpComma = false;
					var trailingCommaTok = false;

					while (!done)
					{
						var tok = this.peek();
						if (value_or_unary) // if we are expecting a value or a unary operator...
						{
							value_or_unary = false; // assume we find a value and are moving to possible binary op
							if (this.isNextRident())
								stack.push(this.RetrieveRident());
							else if (this.isNextSpecial('('))
							{
								// we mark parens in order to avoid incorrectly upgrading an expression
								// like:   a < (b < c)  into  (a < b < c)
								// or:     a, (b, c)    into  (a, b, c)
								// also to ensure we don't do strange things with a parenthesized else
								// like:   a then (b else c)

								// MYTODO: double-confirm that inner paren'ed expressions can have commas and trailing
								// commas no matter what (as shown by "true, true" in line below)

								var leftParenTok = this.next(); // consume the paren
								var innerExpr = this.ExpressionHead(true, [], [], true, true);
								var rightParenTok = this.expectSpecial(')');
								stack.push(innerExpr
									.updateData({
											hasParens: true,
											leftParenTok: leftParenTok,
											rightParenTok: rightParenTok
										}));
							}
							else if (this.isNextSpecial('{')) // table
								stack.push(this.TableLiteral());
							else if (this.isNextSpecial('[')) // list
								stack.push(this.ListLiteral());
							else if (this.isNextSpecial('&')) // anonymous function
								stack.push(this.FunctionLiteral());
							else if (this.isNextSpecial('>>'))
								stack.push(this.LabelLiteral());
							else if (tok.isRatio())
								stack.push(this.RatioLiteral());
							else if (tok.isBeginText())
								stack.push(this.TextLiteral());
							else if (tok.isBeginBlob())
								stack.push(this.BlobLiteral());
							else if (typeof tok.chars !== 'undefined' && config.isUnaryOp(tok.chars))
							{
								op_stack.unshift(tree.createNode('UnaryOp', { op: this.next() }));
								value_or_unary = true; // oops, assumed wrong, stay in this state
							}
							else
							{
								if (canTrailComma && // if we allow trailing commas,
									op_stack.length > 0 && // and we have an operator on the stack,
									op_stack[0].getData().op.chars === ',') // and it's a comma...
								{
									// then we found our trailing comma (that we ignore), so just remove
									// it and break out
									trailingCommaTok = op_stack.shift(); // remove comma
									// value_or_unary is already false, which is where it should be
									done = true; // break out of the loop
								}
								else
									throw 'MYTODO: invalid expression';
							}
						}
						else // need to look for a possible binary op, or abort if none found
						{    // expecting optional indexing, binary operator, function call, or nothing
							if (typeof tok.chars !== 'undefined')
							{
								if (config.isBinaryOp(tok.chars)) // check for binary operator
								{
									if (tok.chars === config.ternaryTestOp)
										thenCount++;
									else if (tok.chars === config.ternaryElseOp)
									{
										if (thenCount > 0)
											thenCount--;
										else
										{
											// we've read too many 'else's, so it must belong to something
											// outside this expression... break out
											done = true;
										}
									}
									else if (!canComma && tok.chars === ',')
									{
										// if we can't have commas, then break out if we find one
										done = true;
									}

									if (!done)
									{
										tok = this.next();
										while (op_stack.length > 0 &&
											this.evalLeftFirst(op_stack[0].getName() == 'UnaryOp', op_stack[0].getData().op.chars, tok.chars))
										{
											var op = op_stack.shift();
											if (op.getName() == 'BinaryOp')
											{
												var right = stack.pop();
												var left = stack.pop();
												op = this.processBinaryOp(op, left, right);
											}
											else // UnaryOp
												op.setTree('UnaValue', stack.pop());
											stack.push(op);
										}
										op_stack.unshift(tree.createNode('BinaryOp', { op: tok }));
										value_or_unary = true;
									}
								}
								else // check for indexing/calling of stack value
								{
									var func = this.lookupTrailingFunc(tok.chars);
									if (func !== false)
										stack[stack.length - 1] = func(stack[stack.length - 1]);
									else
										done = true;
								}
							}
							else
								done = true;
						}
					}

					// if still expecting a value or unary operator, then error out
					if (value_or_unary)
						throw 'MYTODO: invalid expression';

					while (op_stack.length > 0)
					{
						var op = op_stack.shift();
						if (op.getName() == 'BinaryOp')
						{
							var right = stack.pop();
							var left = stack.pop();
							op = this.processBinaryOp(op, left, right);
						}
						else // UnaryOp
							op.setTree('UnaValue', stack.pop());
						stack.push(op);
					}

					if (stack.length != 1) // not sure if this is possible?
						throw 'MYTODO: internal error; invalid expression..?';

					if (canTrailComma)
						stack[0].updateData({ trailingCommaTok: trailingCommaTok });

					return stack[0];
				},

			LabelLiteral: function() {
					// allow a >>depart literal that is always defined as end of function
					// allow a >>next literal that is defined as the next iteration
					// allow a >>exit literal that is defined as the exit of the iteration
					// >>here is always defined as well, but it isn't a keyword, so expectRident will catch it
					if (!this.isNextSpecial('>>'))
						throw 'MYTODO: expecting label literal';
					var arrow = this.next(); // consume the >>
					var val, spc;
					if (this.isNextKeyword('depart') ||
						this.isNextKeyword('next') ||
						this.isNextKeyword('exit'))
					{
						val = this.next();
						spc = true;
					}
					else
					{
						val = this.expectRident();
						spc = val.chars == 'here';
					}
					return tree.createNode('LabelLiteral',
						{ value: val, arrowTok: arrow, specialLabel: spc });
				},

			RatioLiteral: function() {
					if (!this.peek().isRatio())
						throw 'MYTODO: expecting ratio literal';
					return tree.createNode('RatioLiteral', { value: this.next() })
				},

			TextLiteral: function() {
					var bt = this.next();
					if (bt === false || !bt.isBeginText())
						throw 'MYTODO: expecting text literal';
					var t = tree.createBox('TextLiteral', { beginTok: bt });
					while (true)
					{
						var tok = this.next();
						if (tok === false)
							throw 'MYTODO: text literal didn\'t end';
						if (tok.isEndText())
							return t.updateData({ endTok: tok });
						else if (tok.isBeginInnerExpr())
						{
							t.addTree(this.ExpressionTrailComma());
							tok = this.next();
							if (tok === false)
								throw 'MYTODO: text literal has open inner expression';
							if (!tok.isEndInnerExpr())
								throw 'MYTODO: expecting end of inner expression';
						}
						else if (tok.isText())
							t.addTree(tree.createNode('Text', { value: tok }));
						else
							throw 'MYTODO: invalid token inside of text: ' + tok;
					}
				},

			BlobLiteral: function() {
					var bb = this.next();
					if (bb === false || !bb.isBeginBlob())
						throw 'MYTODO: expecting blob literal';
					var t = tree.createBox('BlobLiteral', { beginTok: bb });
					while (true)
					{
						var tok = this.next();
						if (tok === false)
							throw 'MYTODO: blob literal didn\'t end';
						if (tok.isEndBlob())
							return t.updateData({ endTok: tok });
						else if (tok.isBeginInnerExpr())
						{
							t.addTree(this.ExpressionTrailComma());
							tok = this.next();
							if (tok === false)
								throw 'MYTODO: blob literal has open inner expression';
							if (!tok.isEndInnerExpr())
								throw 'MYTODO: expecting end of inner expression';
						}
						else if (tok.isBlob())
							t.addTree(tree.createNode('Blob', { value: tok }));
						else
							throw 'MYTODO: invalid token inside of blob: ' + tok;
					}
				},

			ListLiteral: function() { // read a [ list, ], return a tree
					this.expectSpecial('[');
					var t = tree.createNode('ListLiteral')
					if (!this.isNextSpecial(']'))
						t.setTree('ListExpression', this.ExpressionTrailComma());
					this.expectSpecial(']');
					return t;
				},

			TableKey: function() {
					var p = this.peek();
					if (p === false)
						throw 'MYTODO: expecting table key, instead EOF';

					if (p.isSpecial() && p.chars === '[')
					{
						this.next(); // consume '['
						var e = this.ExpressionNoComma();
						this.expectSpecial(']');
						return tree.createNode('TableKeyExpression').setTree('Expression', e);
					}
					else if (p.isBeginText())
						return tree.createNode('TableKeyText').setTree('TextLiteral', this.TextLiteral());
					else if (p.isBeginBlob())
						return tree.createNode('TableKeyBlob').setTree('BlobLiteral', this.BlobLiteral());
					else if (p.isIdent())
						return tree.createNode('TableKeyName', { name: this.expectIdent() });

					throw 'MYTODO: expecting table key';
				},

			TableLiteral: function() { // read a { table }, return a tree
					var t = tree.createBox('TableLiteral', { openTok: this.expectSpecial('{') });

					// read each key:value pair
					while (!this.isNextSpecial('}'))
					{
						var key = this.TableKey();

						if (this.isNextSpecial(':'))
						{
							this.next(); // consume ':'
							t.toAddNode('TableKeyValue')
								.setTree('TableKey', key)
								.setTree('TableValue', this.ExpressionNoComma());
						}
						else
						{
							t.toAddNode('TableKeyNull')
								.setTree('TableKey', key);
						}

						if (!this.isNextSpecial(','))
							break;

						this.next(); // consume ','
					}

					return t.updateData({ closeTok: this.expectSpecial('}') });
				},

			FunctionLiteral: function() { // read a &function, return a tree
					var t = tree.createNode('FunctionLiteral');
					if (this.isNextSpecial('&')) // optional & to allow for "type code() body end"
					{
						t.updateData({ ampTok: this.next() }); // consume &
						if (this.isNextIdent())
							t.updateData({ internalName: this.expectIdent() });
					}

					// get the input parameters (required)
					t.setTree('InputParamList', this.InputParamList('(', ')'));
					if (this.isNextSpecial('=>'))
					{
						t.updateData({ outTok: this.next() }); // consume the '=>'
						if (this.isNextIdent())
							t.updateData({ outputName: this.expectIdent() });
						t.setTree('OutputParamList', this.OutputParamList());
					}

					return t.setTree('FunctionStatementList', this.StatementList(false))
						.updateData({ endTok: this.expectKeyword('end') })
				},

			InputParamList: function(wrapLeft, wrapRight) { // read (|name, type name, name: expr, type name: expr|) list
					// wrapLeft and wrapRight are the characters that wrap the input list...
					// used to specify either the normal format, or the `as` format:
					//   normal: "(list)"
					//   `as`:   "|list|"
					var foundVarg = false;
					var t = tree.createBox('InputParamList',
						{ wrapLeftTok: this.expectSpecial(wrapLeft) });
					while (!this.isNextSpecial(wrapRight))
					{
						var notn = this.NameOrTypeName(!foundVarg); // notn ==> (N)ame (O)r (T)ype (N)ame
						var init = false;
						if (this.isNextSpecial(':')) // check for initializer
						{
							this.next(); // consume ':'
							init = this.ExpressionNoComma();
						}

						t = t.toAddNode('InputParam', { name: notn.name });
						if (notn.type !== false)
							t.setTree('Type', notn.type);
						if (init !== false)
							t.setTree('InitExpression', init);
						t = t.toParent();

						foundVarg = notn.ddd;

						if (!this.isNextSpecial(','))
							break;
						this.next(); // consume the ','
					}
					return t.updateData({ wrapRightTok: this.expectSpecial(wrapRight) });
				},

			OutputParamList: function() {
					var foundVarg = false;
					var t = tree.createBox('OutputParamList',
						{ wrapLeftTok: this.expectSpecial('(') });
					while (!this.isNextSpecial(')'))
					{
						var totn = this.TypeOrTypeName(!foundVarg); // totn ==> (T)ype (O)r (T)ype (N)ame
						var init = false;
						if (this.isNextSpecial(':')) // check for initializer
						{
							this.next(); // consume the ':'
							init = this.ExpressionNoComma();
						}

						t = t.toAddNode('OutputParam')
							.setTree('Type', totn.type);
						if (totn.name !== false)
							t.updateData({ name: totn.name });
						if (init !== false) // has init
							t.setTree('InitExpression', init);
						t = t.toParent();

						foundVarg = totn.ddd;

						if (!this.isNextSpecial(','))
							break;
						this.next(); // consume the ','
					}
					return t.updateData({ wrapRightTok: this.expectSpecial(')') });
				},

			NameOrTypeName: function(canDDD) { // looks for "name" or "type name"
					if (canDDD && this.isNextSpecial('...'))
					{
						this.next(); // consume the '...'
						var typeTree = this.PartialType(tree.createNode('DotDotDot'));
						return {
								type: typeTree,
								name: this.expectIdent(),
								ddd: true
							};
					}
					var rident = this.RetrieveRident();
					if (this.isNextSpecial('#[') || this.isNextSpecial('.')) // definitely dealing with a type
					{
						var typeTree = this.PartialType(rident);
						return {
								type: typeTree,
								name: this.expectIdent(),
								ddd: false
							};
					}
					// otherwise, only type if next is ident
					if (this.isNextIdent())
					{
						return {
								type: rident,
								name: this.expectIdent(),
								ddd: false
							};
					}
					// nothing else found, therefore it must be a name without a type
					// confirm the rident is really an ident
					rident = rident.getData().rident;
					if (!rident.isIdent())
						throw 'MYTODO: expecting identifier for parameter name (instead found "' + rident.chars + '")';
					return {
							type: false,
							name: rident,
							ddd: false
						};
				},

			TypeOrTypeName: function(canDDD) { // looks for "type" or "type name"
					var typeTree;
					var isDDD = false;
					if (canDDD && this.isNextSpecial('...'))
					{
						this.next(); // consume the '...'
						typeTree = this.PartialType(tree.createNode('DotDotDot'));
						isDDD = true;
					}
					else
						typeTree = this.Type();
					if (this.isNextIdent())
					{
						return {
								type: typeTree,
								name: this.expectIdent(),
								ddd: isDDD
							};
					}
					return {
							type: typeTree,
							name: false,
							ddd: isDDD
						};
				},

			Type: function() { // read a type, return a tree
					return this.PartialType(this.RetrieveRident());
				},

			PartialType: function(t) { // read a type (provided the beginning of the type), return a tree
					while (true)
					{
						if (this.isNextSpecial('#['))
							t = this.IndexTemplate(t);
						else if (this.isNextSpecial('.'))
							t = this.IndexStatic(t);
						else
							return t;
					}
				},

			lookupTrailingFunc: function(chars) { // lookup what trailing function needs to be performed, return the function, or false
					var my = this;
					switch (chars)
					{
					case '.':  return function(t) { return my.IndexStatic(t); };
					case '#[': return function(t) { return my.IndexTemplate(t); };
					case '(':  return function(t) { return my.CallWithList(t); };
					case '{':  return function(t) { return my.CallWithTable(t); };
					case '?:': return function(t) { return my.IndexExistName(t); };
					case '?[': return function(t) { return my.IndexExistExpression(t); };
					case ':':  return function(t) { return my.IndexDynamicName(t); };
					case '[':  return function(t) { return my.IndexDynamicExpression(t); };
					}
					return false;
				},

			IndexStatic: function(t) { // "foo.bar"
					return tree.createNode('IndexStatic', { indexTok: this.expectSpecial('.') })
						.setTree('IndexOf', t)
						.updateData({ rident: this.expectRident() });
				},

			IndexTemplate: function(t) { // "foo#[bar]"
					t = tree.createNode('IndexTemplate', { openTok: this.expectSpecial('#[') })
						.setTree('IndexOf', t)
						.toSetBox('IndexTemplateTypeList');
					while (true)
					{
						if (this.isNextSpecial(']'))
							break;
						t.addTree(this.Type());
						if (!this.isNextSpecial(','))
							break;
						this.next(); // consume ','
					}
					return t.toParent().updateData({ closeTok: this.expectSpecial(']') });
				},

			CallWithList: function(t) { // "foo(bar)"
					var openTok = this.expectSpecial('(');
					if (this.isNextSpecial(')'))
					{
						return tree.createNode('CallWithEmpty',
								{ openTok: openTok, closeTok: this.expectSpecial(')') })
							.setTree('CallOf', t);
					}

					var paramList = this.ExpressionTrailComma();
					var closeTok = this.expectSpecial(')');
						
					return tree.createNode('CallWithList',
							{ openTok: openTok, closeTok: closeTok })
						.setTree('CallOf', t)
						.setTree('ParamList', paramList);
				},

			CallWithTable: function(t) { // "foo{bar}"
					return tree.createNode('CallWithTable')
						.setTree('CallOf', t)
						.setTree('ParamTable', this.TableLiteral());
				},

			IndexExistName: function(t) { // "foo?:bar"
					return tree.createNode('IndexExistName', { indexTok: this.expectSpecial('?:') })
						.setTree('IndexOf', t)
						.updateData({ name: this.expectIdent() });
				},

			IndexExistExpression: function(t) { // "foo?[bar]"
					return tree.createNode('IndexExistExpression', { openTok: this.expectSpecial('?[') })
						.setTree('IndexOf', t)
						.setTree('IndexExistExpressionId', this.ExpressionTrailComma())
						.updateData({ closeTok: this.expectSpecial(']') });
				},

			IndexDynamicName: function(t) { // "foo:bar"
					return tree.createNode('IndexDynamicName', { indexTok: this.expectSpecial(':') })
						.setTree('IndexOf', t)
						.updateData({ name: this.expectIdent() });
				},

			IndexDynamicExpression: function(t) { // "foo[bar]"
					return tree.createNode('IndexDynamicExpression', { openTok: this.expectSpecial('[') })
						.setTree('IndexOf', t)
						.setTree('IndexDynamicExpressionId', this.ExpressionTrailComma())
						.updateData({ closeTok: this.expectSpecial(']') });
				},

			PostExpression: function(t) { // optional assignment/mutation/"as"
					if (this.isNextAssignOp())
					{
						return tree.createNode('AssignMutate', { op: this.next() })
							.setTree('LeftValue', t)
							.setTree('RightValue', this.Expression());
					}
					else if (this.isNextKeyword('as'))
					{
						return tree.createNode('AsBlock', { asTok: this.expectKeyword('as') })
							.setTree('LeftValue', t)
							.toSetNode('AsBlockFunction')
								.setTree('InputParamList', this.InputParamList('|', '|'))
								.setTree('BlockStatementList', this.StatementList(false))
							.toParent()
							.updateData({ endTok: this.expectKeyword('end') });
					}
					return t;
				}
		};
}

function parseRho_parse(tokens)
{
	var par_obj = parseRho_createParseObj(tokens);

	if (tokens.length < 1 || !par_obj.peek().isVersion())
	{
		// MYTODO: serious issue... first token must be version
		console.log(tokens);
		throw 'fatal error';
	}

	// grab the Version token
	var ver_tok = par_obj.next();

	// inform the parser object of the version
	par_obj.setVersion(ver_tok);

	// create the main tree, with the version embedded in it, 
	return tree
		.createNode('RhoAST', { version: ver_tok })
		.setTree('Program', par_obj.StatementList(true));
}

exports.parse = parseRho_parse;

